﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NeuralNetwork;
using NeuralNetwork.Neurons;
using NeuralNetwork.Layers;

namespace NeuralNetwork.Training
{
    public class TrainingBackPropagation
    {
        private double factor = 0.5;

        public TrainingBackPropagation()
        { 
        
        }

        public TrainingBackPropagation(double factor)
        {
            this.factor = factor;
        }

        public double Factor
        {
            get { return factor; }
            set { factor = value; }
        }

        private void BackPropagate(Network network, Test test)
        {
            if (network.layers.Count <= 1) return;
            
            // Reset Error in network
            foreach (Layer l in network.layers)
                foreach (Neuron n in l.neurons)
                    n.Error = 0;

            // Set the Error of the output layer
            double error = 0;
            OutputNeuron on = null;
            for (int i = 0; i < network.output.neurons.Count; i++)
            {
                on = (OutputNeuron)(network.output.neurons[i]);
                if (test.outputs[i] != null)
                    error = test.outputs[i].ToDouble() - on.Value;
                else
                    error = -on.Value;
                on.Error = error * network.activation.Derivative(on.Value);
            }

            // Calculate the error in the remaining layers backwards
            Layer rl = null;
            for (int i = network.layers.Count - 1; i >= 0; i--)
            { 
                rl = network.layers[i];
                foreach (Neuron nb in rl.neurons)
                {
                    foreach (Axon a in nb.dendrites)
                    {
                        a.From.Error += a.Weight * nb.Error;
                    }
                }                                    
            }

            // Adjust weights forwards
            double d = 0;
            foreach (Layer l in network.layers)
            {
                foreach (Neuron n in l.neurons)
                {
                    d = factor * n.Error * n.Value;
                    if (d != 0) 
                    {
                        foreach (Axon a in n.axons) a.AdjustWeight(d);
                    }
                }
            }
        }

        public void Train(Network network,
                          List<Test> learningSets,
                          double limit,
                          int maxIterations)
        {
            double sum = 0;
            double evaluation = limit;
            int iteration = 1;

            network.log.Print("Starting Training using Back Propagation", Log.LogLevel.LOG_INFO);

            while ((evaluation >= limit) && (maxIterations >= iteration))
            {
                network.log.Print("-----------------------------------------------------------", Log.LogLevel.LOG_INFO);
                network.log.Print("- Iteration : " + iteration.ToString().Trim(), Log.LogLevel.LOG_INFO);

                // Train
                foreach (Test test in learningSets)
                {
                    network.input.Observe(test.input);
                    BackPropagate(network, test);
                    network.Prune();
                    network.Reset();
                }

                // Evaluate
                sum = 0;
                foreach (Test ls in learningSets)
                {
                    network.input.Observe(ls.input);
                    sum += network.output.Error(ls.outputs);
                    network.Reset();
                }
                evaluation = Math.Abs(sum / (double)learningSets.Count);
                network.log.Print("- Error : " + evaluation.ToString("F3").Trim(), Log.LogLevel.LOG_INFO);

                // Next Iteration
                iteration++;
            }

            network.log.Print("-----------------------------------------------------------", Log.LogLevel.LOG_INFO);
            network.log.Print("Learning completed", Log.LogLevel.LOG_INFO);
        }
    }
}
